Split out commands so we can just import host commands from realm defined commands#4432
Split out commands so we can just import host commands from realm defined commands#4432tintinthong wants to merge 9 commits intomainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ebe1ca5f0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const { results: atomicResults } = await new ExecuteAtomicOperationsCommand( | ||
| this.commandContext, | ||
| ).execute({ realmUrl, operations }); |
There was a problem hiding this comment.
Preserve stale-spec guidance on atomic install failures
When execute-atomic-operations fails during listing install, the error is now propagated verbatim. This drops the listing-specific handling for the known stale-spec case (filter refers to a nonexistent type), so users get a low-level backend error instead of the actionable "Update Specs" instruction and can’t recover from install failures without manual debugging.
Useful? React with 👍 / 👎.
| if (!doc || !('data' in doc)) { | ||
| throw new Error('We are only expecting single documents returned'); | ||
| } | ||
| delete doc.data.id; | ||
| delete doc.included; | ||
| let cardResource: LooseCardResource = doc?.data; | ||
| delete (doc as any).data.id; |
There was a problem hiding this comment.
Validate fetched card JSON as single-card document
This check only verifies that a data key exists, then treats doc.data as a single card resource. If FetchCardJsonCommand returns a collection-style JSONAPI payload (data array) or another non-single shape, we will construct invalid atomic add operations instead of failing early, which makes installs fail later with harder-to-diagnose errors.
Useful? React with 👍 / 👎.
Preview deployments |
Host Test Results2 277 tests +19 2 262 ✅ +19 2h 32m 31s ⏱️ + 1m 34s Results for commit f39aebd. ± Comparison against base commit 9b7a4b5. This pull request removes 1 and adds 20 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Pull request overview
This PR splits several host-side behaviors into dedicated HostBaseCommand implementations so that realm-defined commands can import and invoke host commands (via the host command shims) instead of reaching into host services directly.
Changes:
- Introduces new host commands for shared operations (realm validation, authed fetch, module-list sanitization, store add, atomic ops execution, etc.) and registers them in the host command shim/index.
- Refactors multiple listing-related commands to call these new commands rather than injecting host services.
- Extends
packages/base/command.gtswith new input/result card definitions for the added commands.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/host/app/commands/validate-realm.ts | New command to normalize and validate realm URLs against available realms. |
| packages/host/app/commands/store-add.ts | New command wrapper around store add behavior. |
| packages/host/app/commands/sanitize-module-list.ts | New shared dependency/module URL filtering + dedupe command. |
| packages/host/app/commands/persist-module-inspector-view.ts | New command wrapper to persist module inspector view selection. |
| packages/host/app/commands/listing-use.ts | Refactors realm validation to use ValidateRealmCommand. |
| packages/host/app/commands/listing-update-specs.ts | Refactors dependency fetching and sanitization to use AuthedFetchCommand + SanitizeModuleListCommand. |
| packages/host/app/commands/listing-remix.ts | Refactors realm validation + module inspector view persistence into commands. |
| packages/host/app/commands/listing-install.ts | Refactors realm validation, source/card fetch, atomic ops execution, and serialization to command wrappers. |
| packages/host/app/commands/listing-generate-example.ts | Uses GetDefaultWritableRealmCommand instead of direct RealmService access. |
| packages/host/app/commands/listing-create.ts | Refactors catalog realm discovery, dependency fetch/sanitize, store add, and card loading into commands. |
| packages/host/app/commands/listing-action-init.ts | Removes direct host service injections (part of command-splitting refactor). |
| packages/host/app/commands/listing-action-build.ts | Removes direct host service injections (part of command-splitting refactor). |
| packages/host/app/commands/index.ts | Registers/shims newly introduced host commands and adds them to HostCommandClasses. |
| packages/host/app/commands/get-realm-of-url.ts | New command to compute containing realm URL for an absolute URL. |
| packages/host/app/commands/get-default-writable-realm.ts | New command to expose default writable realm path. |
| packages/host/app/commands/get-catalog-realm-urls.ts | New command to expose catalog realm URL list. |
| packages/host/app/commands/get-available-realm-urls.ts | New command to expose available realm URL list. |
| packages/host/app/commands/fetch-card-json.ts | New command to fetch a card JSON document by URL. |
| packages/host/app/commands/execute-atomic-operations.ts | New command to execute atomic operations and return atomic results. |
| packages/host/app/commands/can-read-realm.ts | New command to check read permissions for a realm. |
| packages/host/app/commands/authed-fetch.ts | New command wrapper around authenticated fetch returning {ok,status,body}. |
| packages/base/command.gts | Adds new input/result card types needed by the new host commands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let writtenFiles = (atomicResults as Array<Record<string, any>>).map( | ||
| (r) => r.data?.id, | ||
| ); |
There was a problem hiding this comment.
writtenFiles is derived via r.data?.id, which can yield undefined entries when the atomic-op result shape differs (or data is missing). Consider filtering out falsy ids (or validating the result shape) before logging so the debug output accurately reflects what was written.
| let writtenFiles = (atomicResults as Array<Record<string, any>>).map( | |
| (r) => r.data?.id, | |
| ); | |
| let writtenFiles = (atomicResults as Array<Record<string, any>>) | |
| .map((r) => r.data?.id) | |
| .filter((id): id is string => typeof id === 'string' && id.length > 0); |
| this.operatorModeStateService.persistModuleInspectorView( | ||
| input.codePath, | ||
| input.moduleInspectorView as 'schema' | 'spec' | 'preview', | ||
| ); |
There was a problem hiding this comment.
moduleInspectorView is cast to 'schema' | 'spec' | 'preview' without runtime validation. Since the input card field is a plain StringField, an unexpected value can still be passed through and persisted to localStorage, potentially breaking later reads of the selection. Consider validating the input against the allowed set and throwing a clear error for invalid values instead of using a type cast.
| const normalized = trimExecutableExtension(new URL(m)).href; | ||
| if (!seen.has(normalized)) { | ||
| seen.set(normalized, m); |
There was a problem hiding this comment.
SanitizeModuleListCommand currently assumes every entry in input.moduleUrls is a valid absolute URL. new URL(m) will throw on any invalid/relative value, which will abort the entire command rather than filtering the bad entry (the previous inline sanitizers used try/catch per dep). Consider wrapping URL parsing/normalization in a per-item try/catch and skipping invalid module URLs instead of failing the whole command.
| const normalized = trimExecutableExtension(new URL(m)).href; | |
| if (!seen.has(normalized)) { | |
| seen.set(normalized, m); | |
| try { | |
| const normalized = trimExecutableExtension(new URL(m)).href; | |
| if (!seen.has(normalized)) { | |
| seen.set(normalized, m); | |
| } | |
| } catch { | |
| // Skip invalid or relative module URLs during sanitization |
| let body: Record<string, any> = {}; | ||
| if (response.ok) { | ||
| try { | ||
| body = await response.json(); | ||
| } catch { | ||
| // non-JSON response | ||
| } | ||
| } |
There was a problem hiding this comment.
AuthedFetchCommand swallows JSON parse errors and still returns { ok: true, body: {} } when response.ok but the body isn't valid JSON. This can mask server issues and lead callers to proceed with an empty object. Consider either (a) returning the raw text when JSON parsing fails, (b) exposing a parseError/isJson flag, or (c) treating "ok" as false (or throwing) when an acceptHeader indicates the caller expects JSON but parsing fails.
| ): Promise<BaseCommandModule.GetRealmOfUrlResult> { | ||
| let commandModule = await this.loadCommandModule(); | ||
| const { GetRealmOfUrlResult } = commandModule; | ||
| const realmUrl = this.realm.realmOfURL(new URL(input.url)); |
There was a problem hiding this comment.
GetRealmOfUrlCommand calls new URL(input.url) without handling parse failures. Since this command is used as part of module/dependency sanitization, a single invalid/relative URL string would throw and interrupt the whole flow. Consider catching URL construction errors and returning an empty realmUrl (or a non-ok result) instead of throwing.
| const realmUrl = this.realm.realmOfURL(new URL(input.url)); | |
| let parsedUrl: URL; | |
| try { | |
| parsedUrl = new URL(input.url); | |
| } catch (_error) { | |
| return new GetRealmOfUrlResult({ realmUrl: '' }); | |
| } | |
| const realmUrl = this.realm.realmOfURL(parsedUrl); |
| @@ -141,13 +107,16 @@ export default class ListingCreateCommand extends HostBaseCommand< | |||
| relationships, | |||
| meta: { | |||
| adoptsFrom: { | |||
| module: `${this.catalogRealm}catalog-app/listing/listing`, | |||
| module: `${catalogRealm}catalog-app/listing/listing`, | |||
| name: listingSubClass[listingType], | |||
| }, | |||
| }, | |||
There was a problem hiding this comment.
getCatalogRealm() can return undefined, but the result is interpolated into the adoptsFrom module URL. If no catalog realm is found, this will generate a malformed URL like undefinedcatalog-app/... and lead to hard-to-debug downstream errors. Consider validating catalogRealm and throwing a clear error when it is missing (or falling back to a known default).
| let { document: doc } = await new FetchCardJsonCommand( | ||
| this.commandContext, | ||
| ).execute({ url: sourceCard.id }); | ||
| if (!doc || !('data' in doc)) { | ||
| throw new Error('We are only expecting single documents returned'); | ||
| } | ||
| delete doc.data.id; | ||
| delete doc.included; | ||
| let cardResource: LooseCardResource = doc?.data; | ||
| delete (doc as any).data.id; | ||
| delete (doc as any).included; | ||
| let cardResource: LooseCardResource = (doc as any) | ||
| .data as LooseCardResource; |
There was a problem hiding this comment.
The new document-shape check (!doc || !('data' in doc)) is much weaker than the previous isSingleCardDocument validation. This can allow non–single-card documents (or unexpected shapes) through and then build atomic operations with invalid data, causing server-side failures or corrupted writes. Consider restoring isSingleCardDocument (or an equivalent structural check) before mutating/using doc.data.
| const { results: atomicResults } = await new ExecuteAtomicOperationsCommand( | ||
| this.commandContext, | ||
| ).execute({ realmUrl, operations }); | ||
|
|
There was a problem hiding this comment.
ListingInstallCommand previously translated a known atomic-ops failure (filter refers to a nonexistent type) into a more actionable, user-facing error. With the refactor to ExecuteAtomicOperationsCommand, that special-case is lost and users may now see a low-level server detail. Consider reintroducing that mapping either here (around the execute call) or inside ExecuteAtomicOperationsCommand so the UX doesn't regress.
habdelra
left a comment
There was a problem hiding this comment.
probably we should have dedicated tests for the new lower level commands
No description provided.